/*
 * Copyright (c) 2008-2012, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "tansu_internal.h"
#include "tansu_status.h"
#include "tansu_socket.h"
#include "tansu_util.h"
#include "tansu_nwif.h"

tansu_status_t
tansu_socket_setnonblocking(int fd, int enabled)
{
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1)
        return errno == EBADF ? TANSU_INVALID_ARGUMENT : TANSU_FAILED;

    if (enabled)
        flags |= O_NONBLOCK;
    else
        flags &= ~O_NONBLOCK;

    if (fcntl(fd, F_SETFL, flags))
        return errno == EBADF ? TANSU_INVALID_ARGUMENT : TANSU_FAILED;

    return TANSU_SUCCESS;
}

char *
tansu_socket_getaddress(const struct sockaddr_storage *ss)
{
    void *src;
    char *address;
    const char *ret;
    int len;

    if (ss->ss_family == AF_INET) {
        len = TANSU_IPv4_ADDR_LEN_MAX + 1;
        src = &((struct sockaddr_in *)ss)->sin_addr;
    } else if (ss->ss_family == AF_INET6) {
        len = TANSU_IPv6_ADDR_LEN_MAX + 1;
        src = &((struct sockaddr_in6 *)ss)->sin6_addr;
#if HAVE_SYS_UN_H
    } else if (ss->ss_family == AF_UNIX) {
        char *path = ((struct sockaddr_un *)ss)->sun_path;
        len = strlen(path) + 1;
        address = malloc(len);
        if (address)
            snprintf(address, len, "%s", path);
        return address;
#endif /* HAVE_SYS_UN_H */
    } else {
        return NULL;
    }

    address = malloc(len);
    if (address) {
        ret = inet_ntop(ss->ss_family, src, address, len);
        if (ret == NULL) {
            free(address);
            address = NULL;
        }
    }

    return address;
}

socklen_t
tansu_socket_length(const struct sockaddr_storage *ss)
{
    if (ss->ss_family == AF_INET)
        return sizeof(struct sockaddr_in);
    else if (ss->ss_family == AF_INET6)
        return sizeof(struct sockaddr_in6);
#if HAVE_SYS_UN_H
    else if (ss->ss_family == AF_UNIX)
        return sizeof(struct sockaddr_un);
#endif /* HAVE_SYS_UN_H */
    else
        return 0;
}

char *
tansu_socket_getlocal(const struct sockaddr_storage* ss)
{
    int fd;
    struct sockaddr_storage local;
    socklen_t len;

    if ((fd = socket(ss->ss_family, SOCK_DGRAM, 0)) == -1)
        return NULL;

    len = tansu_socket_length(ss);
    if ((connect(fd, (const struct sockaddr *)ss, len)) == -1)
        goto out;

    len = sizeof(local);
    if (getsockname(fd, (struct sockaddr *)&local, &len) == -1)
        goto out;

    tansu_socket_close(fd);

    return tansu_socket_getaddress(&local);

out:
    tansu_socket_close(fd);
    return NULL;
}

uint16_t
tansu_socket_getport(const struct sockaddr_storage *ss)
{
    uint16_t port = 0;

    if (ss->ss_family == AF_INET)
        port = ((struct sockaddr_in *)ss)->sin_port;
    else if (ss->ss_family == AF_INET6)
        port = ((struct sockaddr_in6 *)ss)->sin6_port;

    return ntohs(port);
}

/**
 * @retval TANSU_SUCCESS success
 * @retval TANSU_INVALID_ARGUMENT invalid address
 * @retval TANSU_UNSUPPORTED unsupported protocol family
 */
tansu_status_t
tansu_socket_getaddr(int family,
                     const char *address,
                     uint16_t port,
                     struct sockaddr_storage *ss,
                     socklen_t *len);

tansu_status_t
tansu_socket_getaddr(int family,
                     const char *address,
                     uint16_t port,
                     struct sockaddr_storage *ss,
                     socklen_t *len)
{
    int ret;
    void *dest;

    TASSERT(address && ss);
    memset(ss, 0, sizeof(struct sockaddr_storage));

    if (family == AF_INET) {
        struct sockaddr_in *sai = (struct sockaddr_in *)ss;
        dest = &sai->sin_addr;
        sai->sin_port = htons(port);
        *len = sizeof(struct sockaddr_in);
    } else if (family == AF_INET6) {
        struct sockaddr_in6 *sai6 = (struct sockaddr_in6 *)ss;
        dest = &sai6->sin6_addr;
        sai6->sin6_port = htons(port);
        *len = sizeof(struct sockaddr_in6);
#if HAVE_SYS_UN_H
    } else if (family == AF_UNIX) {
        struct sockaddr_un *sun = (struct sockaddr_un *)ss;
        ret = snprintf(sun->sun_path, sizeof(sun->sun_path), "%s", address);
        if (ret >= (int)sizeof(sun->sun_path)) {
            TLOG_ERROR(TLOG_SOCKET, "address too long\n");
            return TANSU_INVALID_ARGUMENT;
        }
        *len = sizeof(struct sockaddr_un);
        return TANSU_SUCCESS;
#endif /* HAVE_SYS_UN_H */
    } else {
        return TANSU_UNSUPPORTED;
    }

    ss->ss_family = family;
    ret = inet_pton(ss->ss_family, address, dest);
    if (ret <= 0) {
        TLOG_ERROR(TLOG_SOCKET, "inet_pton() failed %d\n", ret);
        return TANSU_INVALID_ARGUMENT;
    }

    return TANSU_SUCCESS;
}

int
tansu_socket_accept(int fd, struct sockaddr_storage* ss)
{
    int ret;
    int nfd;
    socklen_t len = ss ? sizeof(struct sockaddr_storage) : 0;

    if ((nfd = accept(fd, (struct sockaddr *)ss, &len)) == -1) {
        TLOG_ERROR(TLOG_SOCKET, "accept() failed %d\n", errno);
        return -1;
    }

    if (tansu_socket_setnonblocking(nfd, 1) != TANSU_SUCCESS) {
        TLOG_ERROR(TLOG_SOCKET, "setnonblocking() failed\n");
        goto out;
    }

    ret = 1;
    setsockopt(nfd, SOL_SOCKET, SO_KEEPALIVE, &ret, sizeof(ret));
    setsockopt(nfd, SOL_SOCKET, SO_REUSEADDR, &ret, sizeof(ret));

    return nfd;

out:

    tansu_socket_close(nfd);
    return -1;
}

int
tansu_socket_listen(int family,
                    int type,
                    const char *address,
                    uint16_t port,
                    int backlog,
                    tansu_status_t *status)
{
    int fd;
    int ret;
    struct sockaddr_storage ss;
    socklen_t len;

    if (address == NULL || backlog < 0) {
        TLOG_ERROR(TLOG_SOCKET, "invalid argument\n");
        TANSU_SET_STATUS(status, TANSU_INVALID_ARGUMENT);
        return -1;
    }

    ret = tansu_socket_getaddr(family, address, port, &ss, &len);
    if (ret != TANSU_SUCCESS) {
        TANSU_SET_STATUS(status, ret);
        return ret;
    }

    if ((fd = socket(ss.ss_family, type, 0)) == -1) {
        TLOG_ERROR(TLOG_SOCKET, "socket() failed %d\n", errno);
        TANSU_SET_STATUS(status, TANSU_SOCKET_ERROR);
        return -1;
    }

    if (tansu_socket_setnonblocking(fd, 1) != TANSU_SUCCESS) {
        TLOG_ERROR(TLOG_SOCKET, "setnonblocking() failed\n");
        TANSU_SET_STATUS(status, TANSU_SOCKET_ERROR);
        goto out;
    }

    ret = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &ret, sizeof(ret));

    if (bind(fd, (struct sockaddr *)&ss, len) == -1) {
        TLOG_ERROR(TLOG_SOCKET, "bind(%s:%d) failed %d\n", address, port, errno);
        TANSU_SET_STATUS(status, TANSU_SOCKET_ERROR);
        goto out;
    }

    if (type == SOCK_STREAM) {
        ret = 1;
        setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &ret, sizeof(ret));

        if (listen(fd, backlog) == -1) {
            TLOG_ERROR(TLOG_SOCKET, "listen() failed %d\n", errno);
            TANSU_SET_STATUS(status, TANSU_SOCKET_ERROR);
            goto out;
        }
    }

    TANSU_SET_STATUS(status, TANSU_SUCCESS);
    return fd;

out:
    tansu_socket_close(fd);
    return -1;
}

tansu_status_t
tansu_socket_joingroup(int fd,
                       int family,
                       const char *group,
                       const char *ifaddr)
{
    int ret;
    struct ip_mreqn mreqn;

    mreqn.imr_ifindex = 0;

    ret = inet_pton(family, group, &mreqn.imr_multiaddr);
    if (ret <= 0) {
        TLOG_ERROR(TLOG_SOCKET, "inet_pton() failed %d\n", ret);
        return TANSU_INVALID_ARGUMENT;
    }

    ret = inet_pton(family, ifaddr, &mreqn.imr_address);
    if (ret <= 0) {
        TLOG_ERROR(TLOG_SOCKET, "inet_pton() failed %d\n", ret);
        return TANSU_INVALID_ARGUMENT;
    }

    ret = setsockopt(fd,
                     IPPROTO_IP,
                     IP_ADD_MEMBERSHIP,
                     &mreqn,
                     sizeof(mreqn));
    if (ret == -1) {
        TLOG_ERROR(TLOG_SOCKET, "setsockopt() failed %d\n", errno);
        return TANSU_FAILED;
    }

    return TANSU_SUCCESS;
}

void
tansu_socket_joingroup_all(int fd,
                           int family,
                           const char *group,
                           struct tansu_nwifs *nwifs)
{
    struct tansu_nwif *nwif;

    STAILQ_FOREACH(nwif, nwifs, entry) {
        if (family == nwif->family)
            if (NWIF_IS_ACTIVE(nwif->flags))
                tansu_socket_joingroup(fd, family, group, nwif->addr);
    }
}

tansu_status_t
tansu_socket_set_multicast(int fd, int loop, int ttl, const char *mcastif)
{
    int ret;
    char param;
    socklen_t len = sizeof(struct in_addr);
    socklen_t addrlen = sizeof(struct sockaddr_in);
    struct sockaddr_storage ss;
    void *inaddr = NULL;

    if (ttl > 255)
        return TANSU_INVALID_ARGUMENT;

    memset(&ss, 0, sizeof(struct sockaddr_storage));
    if (mcastif) {
        ret = inet_pton(AF_INET, mcastif, &((struct sockaddr_in *)&ss)->sin_addr);
        inaddr = &((struct sockaddr_in *)&ss)->sin_addr;
        if (ret <= 0) {
            ret = inet_pton(AF_INET6, mcastif, &((struct sockaddr_in6 *)&ss)->sin6_addr);
            if (ret <= 0)
                return TANSU_INVALID_ARGUMENT;
            inaddr = &((struct sockaddr_in6 *)&ss)->sin6_addr;
            len = sizeof(struct in6_addr);
            addrlen = sizeof(struct sockaddr_in6);
        }
    }

    param = ttl;
    ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &param, sizeof(param));
    if (ret == -1)
        return TANSU_FAILED;

    param = loop ? 1 : 0;
    ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &param, sizeof(param));
    if (ret == -1)
        return TANSU_FAILED;

    if (mcastif == NULL)
        return TANSU_SUCCESS;

    ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, inaddr, len);
    if (ret == -1)
        return TANSU_FAILED;

#if 0 /* might be bound already */
    if (bind(fd, (struct sockaddr *)&ss, addrlen) < 0)
        return TANSU_FAILED;
#endif

    return TANSU_SUCCESS;
}

tansu_status_t
tansu_socket_set_addr(int family,
                      struct sockaddr_storage *ss,
                      const char *addr,
                      uint16_t port)
{
    int ret;

    if (family == AF_INET) {
        struct sockaddr_in *sin = (struct sockaddr_in *)ss;
        ret = inet_pton(family, addr, &sin->sin_addr);
        if (ret <= 0)
            return TANSU_FAILED;
        sin->sin_port = htons(port);
    } else if (family == AF_INET6) {
        struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ss;
        ret = inet_pton(family, addr, &sin6->sin6_addr);
        if (ret <= 0)
            return TANSU_FAILED;
        sin6->sin6_port = htons(port);
    } else {
        return TANSU_NOT_SUPPORTED;
    }
    ss->ss_family = family;

    return TANSU_SUCCESS;
}

#if 0
tansu_status_t
tansu_socket_outaddr(const char *remote, struct sockaddr_storage *ss)
{
    if (remote == NULL || ss == NULL)
        return TANSU_INVALID_ARGUMENT;


}
#endif

